/**
 * Copyright 2006 gworks.com.au
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License. (http://www.apache.org/licenses/LICENSE-2.0)
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 
 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for 
 * the specific language governing permissions and limitations under the License.
 *
 * <dhamma-message src="Atisha (11th century Tibetan Buddhist master)">
 * 	The greatest achievement is selflessness. The greatest worth is self-mastery.
 * 	The greatest quality is seeking to serve others. The greatest precept is continual awareness.
 * 	The greatest medicine is the emptiness of everything. The greatest action is not conforming with the worlds ways.
 * 	The greatest magic is transmuting the passions. The greatest generosity is non-attachment.
 * 	The greatest goodness is a peaceful mind. The greatest patience is humility.
 * 	The greatest effort is not concerned with results. The greatest meditation is a mind that lets go.
 * 	The greatest wisdom is seeing through appearances. 
 * </dhamma-message>
 */
package org.javaongems.client.macctrls;

import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.Label;

/**
 * <p>Provides all the behaviour associated with the fish eye animation.</p>
 * <p>Developer warning:<br/>
 * This was my very first GWT widget that I created just a month after GWT was 
 * first released. At that stage I consider myself lucky to able to spell GWT. 
 * You should know in advance that I am a severely impaired javascript hacker 
 * and I consider this code a minor miracle. This use to be a nested class of
 * FishEyeBar but in a rage of desparation caused by the GWT Compiler's inability
 * to report a root problem I removed all nested classes from my solutions.
 * </p>
 * <p>Just in case you are curious of the dhamma message accompanied on the license,
 * yes it is true; being an early adopter on GWT did it to me.
 * </p>
 * @author Ashin Wimalaeewa (ash)
 * 
 * This code was ported from the Dojo Toolkit::FisheyeList.js:
 * URL: http://svn.dojotoolkit.org/dojo/trunk/src/widget/FisheyeList.js
 * Credits: Cal, Alex, Bill, David & Mumme (from svn log history)
 * Apologies in advance if I have not done the Dojo FisheyeList justice
 * in my implementation. 
 */
public class FishEyeAnimator {
	final static public int CENTER = 0;
	final static public int LEFT = 1;
	final static public int RIGHT = 2;
	final static public int TOP = 3;
	final static public int BOTTOM = 4;
	final protected FishEyeBar bar;
	protected float proximityLeft, proximityTop, proximityBottom, proximityRight;
	protected int anchorEdge = CENTER, labelEdge = BOTTOM;
	protected int itemCount;
	protected float barWidth, barHeight, totalWidth, totalHeight;
	protected float posX = -1, posY = -1, hitX1 = -1, hitY1 = -1, hitX2 = -1, hitY2 = -1;
	protected float timerScale = 1.0f;
	protected boolean conservativeTrigger = false;
	protected int effectUnits = 2;
	protected int itemPadding = 10;
	protected int toolTipDelay = 2500;
	protected ChildInfo[] ci;
	protected boolean isOver;
	protected boolean isAlreadyReset = true;
	protected Label lastActiveLabel;
	protected ExpandSlowlyTask expandSlowlyTask = new ExpandSlowlyTask();

	public FishEyeAnimator(FishEyeBar febar) {
		bar = febar;
		initialiseFromTemplate();
		reorganise();
	}

	public void reorganise() {
		itemCount = bar.items.size();
		if (itemCount < 1)
			return;
		ci = new ChildInfo[itemCount];
		barWidth = (bar.horizontal ? itemCount : 1) * bar.itemWidth;
		barHeight = (bar.horizontal ? 1 : itemCount) * bar.itemHeight;

		totalWidth = proximityLeft + proximityRight + barWidth;
		totalHeight = proximityTop + proximityBottom + barHeight;
		refresh();
	}
	
	public void refresh() {
		calculateEffectRangesForEachItem();
		positionBar();
		positionItems();
		calculateHitGrid();
	}
	
	protected void calculateEffectRangesForEachItem() {
		for (int i = 0; i < itemCount; i++) {
			ci[i] = new ChildInfo();
			ci[i].posX = bar.itemWidth * (bar.horizontal ? i : 0);
			ci[i].posY = bar.itemHeight * (bar.horizontal ? 0 : i);

			ci[i].cenX = ci[i].posX + (bar.itemWidth / 2);
			ci[i].cenY = ci[i].posY + (bar.itemHeight / 2);

			float isz = bar.horizontal ? bar.itemWidth
					: bar.itemHeight;
			float r = effectUnits * isz;
			float c = bar.horizontal ? ci[i].cenX : ci[i].cenY;
			float lhs = bar.horizontal ? proximityLeft : proximityTop;
			float rhs = bar.horizontal ? proximityRight : proximityBottom;
			float siz = bar.horizontal ? barWidth : barHeight;

			float range_lhs = r;
			float range_rhs = r;

			if (range_lhs > c + lhs) 
				range_lhs = c + lhs;
			if (range_rhs > (siz - c + rhs)) 
				range_rhs = siz - c + rhs;

			ci[i].effectRangeLeft = range_lhs / isz;
			ci[i].effectRangeRight = range_rhs / isz;
		}
	}
	
	protected void positionBar() {
		bar.setWidth(barWidth + "px");
		bar.setHeight(barHeight + "px");
	}

	protected void positionItems() {
		for (int i = 0; i < itemCount; i++) {
			FishEyeItem elem = (FishEyeItem) bar.items.get(i);
			positionDomElement(elem, ci[i].posY, ci[i].posX);
			sizeDomElement(elem, bar.itemWidth, bar.itemHeight);

			Element imageElm = elem.getImageElement();
			DOM.setStyleAttribute(imageElm, "top", itemPadding + "%");
			DOM.setStyleAttribute(imageElm, "left", itemPadding + "%");
			DOM.setStyleAttribute(imageElm, "width", (100 - 2 * itemPadding) + "%");
			DOM.setStyleAttribute(imageElm, "height", (100 - 2 * itemPadding) + "%");
		}
	}

	protected void calculateHitGrid() {
		if (itemCount < 1 || bar.getParent() == null)
			return;
		int absX = DOM.getAbsoluteLeft(bar.outer);
		if (absX <= 0) absX = bar.getParent().getAbsoluteLeft();
		int absY = DOM.getAbsoluteTop(bar.outer);
		if (absY <= 0) absY = bar.getParent().getAbsoluteTop();

		hitX1 = absX - proximityLeft;
		hitY1 = absY - proximityTop; 
		hitX2 = hitX1 + totalWidth;
		hitY2 = hitY1 + totalHeight;
	}
	
	public boolean handleMouseEvent(Event e, int pageX, int pageY) {
		boolean inProximity = false;
		if ((pageX >= hitX1) && (pageX <= hitX2) && (pageY >= hitY1)
				&& (pageY <= hitY2)) {
			if (!isOver) 
				setActive(e, pageX, pageY);
			onGridMouseMove(pageX - hitX1, pageY - hitY1);
			inProximity = true;
			isAlreadyReset = false;
		} else {
			if (isOver) 
				setDormant(e);
			if (!isAlreadyReset) {
				positionItems();
				if (lastActiveLabel != null) lastActiveLabel.setVisible(false);
				isAlreadyReset = true;
			}
		}
		return inProximity;
	}
	
	public void handleToolTip(final FishEyeItem itm, boolean show) {
		if (!"".equals(itm.getCaption())) {
			Label lbl = itm.getLabel();
			boolean isCurrVisible = lbl.isVisible();
			if (show && !isCurrVisible) {
				Timer tmr = new Timer() {
					public void run() {
						itm.getLabel().setVisible(false);
					}
				};
				lbl.setVisible(show);
				tmr.schedule(toolTipDelay);
				lastActiveLabel = lbl;
			} else if (!show && isCurrVisible)
				lbl.setVisible(show);
		}
	}

	protected void onGridMouseMove(float x, float y) {
		posX = x; posY = y;
		paint();
	}

	protected void setDormant(Event e) {
		if (!isOver) 
			return;
		isOver = false;
		onGridMouseMove(-1, -1);
	}

	protected void setActive(Event e, int pageX, int pageY) {
		if (isOver) 
			return;
		isOver = true;
		if (conservativeTrigger) {
			timerScale = 0.0f;
			handleMouseEvent(e, pageX, pageY);
			expandSlowly();
		}
	}

	protected void expandSlowly() {
		if (!isOver) 
			return;
		timerScale += 0.2;
		paint();

		if (timerScale < 1.0) 
			expandSlowlyTask.schedule(10);
	}
	
	protected class ExpandSlowlyTask extends Timer {
		public void run() {
			expandSlowly();
		}
	}

	protected void paint() {
		float x = posX;
		float y = posY;

		if (itemCount <= 0) 
			return;
		float pos = bar.horizontal ? x : y;
		float prx = bar.horizontal ? proximityLeft : proximityTop;
		int siz = bar.horizontal ? bar.itemWidth : bar.itemHeight;
		float sim = bar.horizontal 
				? (1.0f - timerScale) * bar.itemWidth + timerScale * bar.itemMaxWidth
				: (1.0f - timerScale) * bar.itemHeight + timerScale * bar.itemMaxHeight;
		float cen = ((pos - prx) / siz) - 0.5f;
		float max_off_cen = (sim / siz) - 0.5f;
		if (max_off_cen > effectUnits) 
			max_off_cen = effectUnits;

		float off_weight = 0;
		if (anchorEdge == BOTTOM) {
			float cen2 = (y - proximityTop) / bar.itemHeight;
			off_weight = (cen2 > 0.5) ? 1 : y / (proximityTop + (bar.itemHeight / 2));
		}
		if (anchorEdge == TOP) {
			float cen2 = (y - proximityTop) / bar.itemHeight;
			off_weight = (cen2 < 0.5) ? 1 : (totalHeight - y) / (proximityBottom + (bar.itemHeight / 2));
		}
		if (anchorEdge == RIGHT) {
			float cen2 = (x - proximityLeft) / bar.itemWidth;
			off_weight = (cen2 > 0.5) ? 1 : x / (proximityLeft + (bar.itemWidth / 2));
		}
		if (anchorEdge == LEFT) {
			float cen2 = (x - proximityLeft) / bar.itemWidth;
			off_weight = (cen2 < 0.5) ? 1 : (totalWidth - x) / (proximityRight + (bar.itemWidth / 2));
		}
		if (anchorEdge == CENTER) {
			if (bar.horizontal)
				off_weight = y / (totalHeight);
			else
				off_weight = x / (totalWidth);

			if (off_weight > 0.5)
				off_weight = 1 - off_weight;
			off_weight *= 2;
		}

		for (int i = 0; i < itemCount; i++) {
			float weight = weightAt(cen, i);
			if (weight < 0) 
				weight = 0;
			setItemSize(i, weight * off_weight);
		}

		// set positions
		int main_p = Math.round(cen);
		int offset = 0;

		if (cen < 0)
			main_p = 0;
		else if (cen > this.itemCount - 1)
			main_p = this.itemCount - 1;
		else
			offset = (int) (cen - main_p) * ((bar.horizontal ? bar.itemWidth 
							: bar.itemHeight) - ci[main_p].sizeMain);
		positionElementsFrom(main_p, offset);
	}

	protected void positionElementsFrom(int p, int offset) {
		int pos = 0;

		FishEyeItem feItm = (FishEyeItem) bar.items.get(p);
		Element elm = feItm.getElement();

		if (bar.horizontal) {
			pos = Math.round(ci[p].usualX + offset);
			DOM.setStyleAttribute(elm, "left", pos + "px");
		} else {
			pos = Math.round(ci[p].usualY + offset);
			DOM.setStyleAttribute(elm, "top", pos + "px");
		}
		positionLabel(ci[p], feItm);

		// position before
		int bpos = pos;
		for (int i = p - 1; i >= 0; i--) {
			FishEyeItem feItmCurr = (FishEyeItem) bar.items.get(i);
			Element elmCurr = feItmCurr.getElement();
			bpos -= ci[i].sizeMain;
			if (bar.horizontal)
				DOM.setStyleAttribute(elmCurr, "left", bpos + "px");
			else
				DOM.setStyleAttribute(elmCurr, "top", bpos + "px");
			positionLabel(ci[i], feItmCurr);
		}

		// position after
		int apos = pos;
		for (int i = p + 1; i < itemCount; i++) {
			FishEyeItem feItmCurr = (FishEyeItem) bar.items.get(i);
			Element elmCurr = feItmCurr.getElement();
			apos += ci[i - 1].sizeMain;
			if (bar.horizontal)
				DOM.setStyleAttribute(elmCurr, "left", apos + "px");
			else
				DOM.setStyleAttribute(elmCurr, "top", apos + "px");
			positionLabel(ci[i], feItmCurr);
		}
	}

	protected void positionLabel(ChildInfo info, FishEyeItem itm) {
		int x = 0, y = 0;
		Label lbl = itm.getLabel();
		Element lblElem = lbl.getElement();
		int labelW = lbl.getOffsetWidth(); 
		int labelH = lbl.getOffsetHeight();
		if (labelEdge == TOP){
			x = Math.round((info.sizeW / 2) - (labelW / 2));
			y = -labelH;
		}
		if (labelEdge == BOTTOM){
			x = Math.round((info.sizeW / 2) - (labelW / 2));
			y = info.sizeH;
		}
		if (labelEdge == LEFT){
			x = -labelW;
			y = Math.round((info.sizeH / 2) - (labelH / 2));
		}
		if (labelEdge == RIGHT){
			x = info.sizeW;
			y = Math.round((info.sizeH / 2) - (labelH / 2));
		}
		DOM.setStyleAttribute(lblElem, "top", y + "px");
		DOM.setStyleAttribute(lblElem, "left", x + "px");
	}

	protected void setItemSize(int p, float scale) {
		scale *= timerScale;
		int w = Math.round(bar.itemWidth + ((bar.itemMaxWidth - bar.itemWidth) * scale));
		int h = Math.round(bar.itemHeight + ((bar.itemMaxHeight - bar.itemHeight) * scale));
		FishEyeItem feItm = (FishEyeItem) bar.items.get(p);
		ci[p].sizeW = w; ci[p].sizeH = h;
		if (bar.horizontal) {
			ci[p].sizeMain = (int) w;
			ci[p].sizeOff = (int) h;
			float y = 0;
			if (anchorEdge == TOP)
				y = (ci[p].cenY - (bar.itemHeight / 2));
			else if (anchorEdge == BOTTOM)
				y = (ci[p].cenY - (h - (bar.itemHeight / 2)));
			else
				y = (ci[p].cenY - (h / 2));
			ci[p].usualX = (int) Math.round(ci[p].cenX - (w / 2));
			positionDomElement(feItm, (int)y, ci[p].usualX);
		} else {
			ci[p].sizeOff = (int) w;
			ci[p].sizeMain = (int) h;
			float x = 0;
			if (anchorEdge == LEFT)
				x = ci[p].cenX - (bar.itemWidth / 2);
			else if (anchorEdge == RIGHT)
				x = ci[p].cenX - (w - (bar.itemWidth / 2));
			else
				x = ci[p].cenX - (w / 2);
			ci[p].usualY = (int) Math.round(ci[p].cenY - (h / 2));
			positionDomElement(feItm, ci[p].usualY, (int)x);
		}
		sizeDomElement(feItm, w, h);
	}

	protected void positionDomElement(FishEyeItem itm, int top, int left) {
		Element outerElement = itm.getElement();
		DOM.setStyleAttribute(outerElement, "top", top + "px");
		DOM.setStyleAttribute(outerElement, "left", left + "px");
	}
	
	protected void sizeDomElement(FishEyeItem itm, int width, int height) {
		itm.setWidth(width + "px");
		itm.setHeight(height + "px");
		Element imgElement = itm.getImageElement();
		DOM.setStyleAttribute(imgElement, "width", width + "px");
		DOM.setStyleAttribute(imgElement, "height", height + "px");
	}
	
	protected float weightAt(float cen, int i) {
		float dist = Math.abs(cen - i);
		float limit = ((cen - i) > 0) ? ci[i].effectRangeRight : ci[i].effectRangeLeft;
		return (dist > limit) ? 0 : (1 - dist / limit);
	}

	protected void initialiseFromTemplate() {
		if (bar.horizontal && anchorEdge == LEFT) 		anchorEdge = CENTER;
		if (bar.horizontal && anchorEdge == RIGHT) 		anchorEdge = CENTER;
		if (!bar.horizontal && anchorEdge == TOP) 		anchorEdge = CENTER;
		if (!bar.horizontal && anchorEdge == BOTTOM)		anchorEdge = CENTER;

		if (labelEdge == CENTER)								labelEdge = TOP;
		if (bar.horizontal && (labelEdge == LEFT))		labelEdge = TOP;
		if (bar.horizontal && (labelEdge == RIGHT))		labelEdge = TOP;
		if (!bar.horizontal && (labelEdge == TOP))		labelEdge = LEFT;
		if (!bar.horizontal && (labelEdge == BOTTOM))	labelEdge = LEFT;

		proximityLeft = (int) (bar.itemWidth * (effectUnits - 0.5));
		proximityRight = (int) (bar.itemWidth * (effectUnits - 0.5));
		proximityTop = (int) (bar.itemHeight * (effectUnits - 0.5));
		proximityBottom = (int) (bar.itemHeight * (effectUnits - 0.5));

		if (anchorEdge == LEFT)									proximityLeft = 0;
		if (anchorEdge == RIGHT)								proximityRight = 0;
		if (anchorEdge == TOP)									proximityTop = 0;
		if (anchorEdge == BOTTOM)								proximityBottom = 0;
		if (anchorEdge == CENTER) {
			proximityLeft /= 2;
			proximityRight /= 2;
			proximityTop /= 2;
			proximityBottom /= 2;
		}
	}

	static protected class ChildInfo {
		public float effectRangeLeft, effectRangeRight;
		public int posX, posY, cenX, cenY, sizeW, sizeH;
		public int sizeMain, sizeOff, usualX, usualY;
	}
}